#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
国际化字符串扫描工具
用于扫描代码库中的中文字符串，帮助识别需要国际化的内容
"""

import os
import re
import argparse
import csv
import chardet
from concurrent.futures import ThreadPoolExecutor
from tqdm import tqdm

# 支持的文件扩展名
SUPPORTED_EXTENSIONS = {
    # 前端文件
    '.html', '.htm', '.js', '.jsx', '.ts', '.tsx', '.vue', 
    # '.css', '.scss', '.less',
    # 后端文件
    '.java', '.py', '.go', '.php', '.rb', '.c', '.cpp', '.cs', '.swift', '.kt',
    # 配置文件
    '.json',
    #  '.xml', 
    #  '.yaml', '.yml', '.properties', '.ini', '.conf',
    # 其他文本文件
    # '.txt',
    #  '.md',
    #   '.log'
}

# 排除的目录
EXCLUDE_DIRS = {
    '.git', '__pycache__', 'node_modules', 'vendor', 'dist', 'build', 'target',
    'logs', 'tmp', 'temp', '.idea', '.vscode', '.svn'
}

# 排除的文件
EXCLUDE_FILES = {
    'package-lock.json', 'yarn.lock', 'Pipfile.lock', 'poetry.lock'
}

class I18nScanner:
    def __init__(self, root_dir, output_file, max_workers=4, exclude_patterns=None):
        self.root_dir = root_dir
        self.output_file = output_file
        self.max_workers = max_workers
        self.exclude_patterns = exclude_patterns or []
        self.results = []
        self.total_files = 0
        self.scanned_files = 0
        
    def is_supported_file(self, filename):
        """检查文件是否为支持的类型"""
        _, ext = os.path.splitext(filename)
        return ext.lower() in SUPPORTED_EXTENSIONS and filename not in EXCLUDE_FILES
    
    def is_excluded(self, path):
        """检查路径是否需要排除"""
        for dir_name in EXCLUDE_DIRS:
            if f"{os.sep}{dir_name}{os.sep}" in path or path.endswith(f"{os.sep}{dir_name}"):
                return True
        
        for pattern in self.exclude_patterns:
            if re.search(pattern, path):
                return True
        
        return False
    
    def detect_encoding(self, file_path):
        """检测文件编码"""
        try:
            with open(file_path, 'rb') as f:
                result = chardet.detect(f.read(10000))  # 只读取文件的前10000字节进行检测
                return result['encoding'] or 'utf-8'
        except:
            return 'utf-8'  # 默认使用utf-8
    
    def scan_file(self, file_path):
        """扫描单个文件中的中文字符串"""
        if not self.is_supported_file(os.path.basename(file_path)):
            return
        
        try:
            encoding = self.detect_encoding(file_path)
            with open(file_path, 'r', encoding=encoding, errors='replace') as f:
                content = f.readlines()
                
            # 定义中文字符的正则表达式模式
            # 1. 匹配字符串中的中文
            patterns = [
                # 双引号字符串中的中文
                (r'"([^"]*[\u4e00-\u9fa5]+[^"]*)"', 'double_quote'),
                # 单引号字符串中的中文
                (r"'([^']*[\u4e00-\u9fa5]+[^']*)'", 'single_quote'),
                # 反引号模板字符串中的中文 (JavaScript/TypeScript等)
                (r'`([^`]*[\u4e00-\u9fa5]+[^`]*)`', 'template_literal'),
                # 多行字符串中的中文 (Python的三引号)
                # (r"""([^"""`]*[\u4e00-\u9fa5]+[^"""`]*)"""", 'multi_line_string'),
                (r"'''([^'''`]*[\u4e00-\u9fa5]+[^'''`]*)'''", 'multi_line_string')
            ]
            
            # 扫描每一行
            for line_num, line in enumerate(content, 1):
                # 处理并移除注释
                line_without_comments = self._remove_comments(line, file_path)
                if not line_without_comments.strip():
                    continue
                
                # 搜索中文字符串
                for pattern, str_type in patterns:
                    matches = re.finditer(pattern, line_without_comments)
                    for match in matches:
                        chinese_text = match.group(1)
                        # 过滤掉只包含少量中文字符的情况
                        chinese_chars = re.findall(r'[\u4e00-\u9fa5]', chinese_text)
                        if len(chinese_chars) >= 1:  # 至少包含一个中文字符
                            # 相对路径（相对于根目录）
                            rel_path = os.path.relpath(file_path, self.root_dir)
                            
                            # 确定模块名称（简单处理，使用目录名）
                            module_name = os.path.dirname(rel_path).replace(os.sep, '.')
                            if not module_name:
                                module_name = 'root'
                            
                            # 获取文件扩展名
                            _, file_extension = os.path.splitext(file_path)
                            file_extension = file_extension.lower()
                            
                            # 添加到结果
                            self.results.append({
                                'module_name': module_name,
                                'file_path': rel_path,
                                'file_extension': file_extension,
                                'line_number': line_num,
                                'text_content': match.group(0),
                                'chinese_text': chinese_text,
                                'string_type': str_type,
                                'complexity': self._evaluate_complexity(chinese_text)
                            })
        except Exception as e:
            print(f"Error scanning file {file_path}: {e}")
        finally:
            self.scanned_files += 1
            
    def _remove_comments(self, line, file_path):
        """移除代码行中的注释，但保留字符串内容"""
        # 获取文件扩展名以确定语言类型
        _, ext = os.path.splitext(file_path)
        ext = ext.lower()
        
        # 处理不同语言的注释格式
        # 对于大多数语言，先处理行内注释
        # 注意：这是一个简化的实现，可能无法处理所有复杂情况
        
        # 先保存字符串内容，避免误删字符串中的注释符号
        # 这是一个简化的实现，主要处理常见情况
        string_markers = []
        result = []
        in_string = False
        string_char = None
        escape = False
        
        for i, char in enumerate(line):
            # 处理转义字符
            if char == '\\' and not escape:
                escape = True
                result.append(char)
                continue
            
            # 处理字符串开始/结束
            if not escape and not in_string and char in ['"', "'", '`']:
                in_string = True
                string_char = char
                string_markers.append((i, char))
            elif not escape and in_string and char == string_char:
                in_string = False
                string_markers.append((i, char))
            
            escape = False
            result.append(char)
        
        # 现在我们有了原始行的副本，接下来根据文件类型移除注释
        line_without_comments = line
        
        # JavaScript, TypeScript, HTML, CSS等
        if ext in ['.js', '.jsx', '.ts', '.tsx', '.html', '.htm', '.css', '.scss', '.less']:
            # 移除行内//注释，但保留字符串中的内容
            if '//' in line and not in_string:
                parts = line.split('//', 1)
                line_without_comments = parts[0]
            # HTML注释 <!-- -->
            if ext in ['.html', '.htm'] and '<!--' in line:
                line_without_comments = re.sub(r'<!--.*?-->', '', line_without_comments)
        # Python
        elif ext == '.py':
            # 移除行内#注释，但保留字符串中的内容
            if '#' in line and not in_string:
                parts = line.split('#', 1)
                line_without_comments = parts[0]
        # Java, C, C++, C#
        elif ext in ['.java', '.c', '.cpp', '.cs']:
            # 移除行内//注释
            if '//' in line and not in_string:
                parts = line.split('//', 1)
                line_without_comments = parts[0]
        # PHP
        elif ext == '.php':
            # 移除行内//和#注释
            if not in_string:
                if '//' in line:
                    parts = line.split('//', 1)
                    line_without_comments = parts[0]
                elif '#' in line:
                    parts = line.split('#', 1)
                    line_without_comments = parts[0]
        # Ruby
        elif ext == '.rb':
            # 移除行内#注释
            if '#' in line and not in_string:
                parts = line.split('#', 1)
                line_without_comments = parts[0]
        # Go
        elif ext == '.go':
            # 移除行内//注释
            if '//' in line and not in_string:
                parts = line.split('//', 1)
                line_without_comments = parts[0]
        # 其他配置文件等
        elif ext in ['.json', '.xml', '.yaml', '.yml', '.properties', '.ini', '.conf']:
            # 根据文件类型处理注释
            if ext in ['.properties', '.ini', '.conf'] and '#' in line:
                parts = line.split('#', 1)
                line_without_comments = parts[0]
            elif ext in ['.yaml', '.yml']:
                if '#' in line and not in_string:
                    parts = line.split('#', 1)
                    line_without_comments = parts[0]
        
        return line_without_comments
    
    def _evaluate_complexity(self, text):
        """评估国际化的复杂度"""
        # 简单的复杂度评估逻辑
        # 包含变量或格式化的文本更复杂
        if re.search(r'\{\{|\}\}|\$\{|\}|\%[sd]', text):
            return 'high'
        # 纯文本相对简单
        return 'low' if len(text) < 20 else 'medium'
    
    def count_files(self):
        """统计需要扫描的文件总数"""
        count = 0
        for root, _, files in os.walk(self.root_dir):
            if self.is_excluded(root):
                continue
            
            for file in files:
                file_path = os.path.join(root, file)
                if self.is_supported_file(file) and not self.is_excluded(file_path):
                    count += 1
        
        self.total_files = count
        return count
    
    def scan(self):
        """开始扫描"""
        print(f"开始扫描代码库: {self.root_dir}")
        print(f"正在统计需要扫描的文件数量...")
        self.count_files()
        print(f"总共需要扫描 {self.total_files} 个文件")
        
        # 创建任务列表
        file_paths = []
        for root, _, files in os.walk(self.root_dir):
            if self.is_excluded(root):
                continue
            
            for file in files:
                file_path = os.path.join(root, file)
                if self.is_supported_file(file) and not self.is_excluded(file_path):
                    file_paths.append(file_path)
        
        # 使用线程池进行并行扫描
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            # 使用tqdm显示进度
            list(tqdm(executor.map(self.scan_file, file_paths), total=len(file_paths)))
        
        # 保存结果
        self.save_results()
        
        print(f"扫描完成！共发现 {len(self.results)} 处需要国际化的文本")
        print(f"结果已保存到: {self.output_file}")
    
    def save_results(self):
        """保存扫描结果到CSV文件"""
        if not self.results:
            print("未发现需要国际化的文本")
            return
        
        # 确保输出目录存在
        output_dir = os.path.dirname(self.output_file)
        if output_dir and not os.path.exists(output_dir):
            os.makedirs(output_dir)
        
        # 写入CSV文件
        with open(self.output_file, 'w', newline='', encoding='utf-8-sig') as f:
            writer = csv.DictWriter(f, fieldnames=[
                'module_name', 'file_path', 'file_extension', 'line_number', 'text_content', 
                'chinese_text', 'string_type', 'complexity'
            ])
            writer.writeheader()
            writer.writerows(self.results)

if __name__ == '__main__':
    # 命令行参数解析
    parser = argparse.ArgumentParser(description='国际化字符串扫描工具')
    parser.add_argument('-d', '--dir', type=str, default='.', help='要扫描的根目录')
    parser.add_argument('-o', '--output', type=str, default='i18n_scan_results.csv', help='输出的CSV文件路径')
    parser.add_argument('-w', '--workers', type=int, default=4, help='并行工作线程数')
    parser.add_argument('-e', '--exclude', type=str, nargs='*', default=[], help='要排除的正则表达式模式')
    
    args = parser.parse_args()
    
    # 创建扫描器并开始扫描
    scanner = I18nScanner(
        root_dir=os.path.abspath(args.dir),
        output_file=os.path.abspath(args.output),
        max_workers=args.workers,
        exclude_patterns=args.exclude
    )
    
    scanner.scan()